﻿// Copyright (c) Microsoft Corporation
//
// All rights reserved. 
//
// Licensed under the Apache License, Version 2.0 (the ""License""); you may not use this file 
// except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING 
// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY 
// OR NON-INFRINGEMENT. 
//
// See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using TED.WindowsAzure.Unplugged.Mobile.Exceptions;
using TED.WindowsAzure.Unplugged.Mobile.Extensions;
using TED.WindowsAzure.Unplugged.Mobile.File;
using TED.WindowsAzure.Unplugged.Mobile.Folder;
using TED.WindowsAzure.Unplugged.Mobile.Sync.Events;
using TED.WindowsAzure.Unplugged.Mobile.Sync.File;
using TED.WindowsAzure.Unplugged.Mobile.Sync.Folder;
using Windows.Storage;
using Windows.Storage.Streams;

namespace TED.WindowsAzure.Unplugged.Mobile.Sync.Storage
{
    internal class SyncEnabledStorage
    {
        private readonly ISyncFileStorage _syncFileStorage;

        #region Events

        /// <summary>
        ///     Raises the <see cref="E:PullStarted" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncStartedEventArgs" /> instance containing the event data.</param>
        protected virtual void OnPullStarted(SyncStartedEventArgs e)
        {
            if (PullStarted != null)
                UIDispatcher.BeginExecute(() => PullStarted(this, e));
        }

        /// <summary>
        ///     Raises the <see cref="E:PullProgressChanged" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncProgressEventArgs" /> instance containing the event data.</param>
        protected virtual void OnPullProgressChanged(SyncProgressEventArgs e)
        {
            if (PullProgressChanged != null)
                UIDispatcher.BeginExecute(() => PullProgressChanged(this, e));
        }

        /// <summary>
        ///     Raises the <see cref="E:PullCompleted" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncCompletedEventArgs" /> instance containing the event data.</param>
        protected virtual void OnPullCompleted(SyncCompletedEventArgs e)
        {
            if (PullCompleted != null)
                UIDispatcher.BeginExecute(() => PullCompleted(this, e));
        }

        /// <summary>
        ///     Raises the <see cref="E:PushStarted" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncStartedEventArgs" /> instance containing the event data.</param>
        protected virtual void OnPushStarted(SyncStartedEventArgs e)
        {
            if (PushStarted != null)
                UIDispatcher.BeginExecute(() => PushStarted(this, e));
        }

        /// <summary>
        ///     Raises the <see cref="E:PushProgressChanged" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncProgressEventArgs" /> instance containing the event data.</param>
        protected virtual void OnPushProgressChanged(SyncProgressEventArgs e)
        {
            if (PushProgressChanged != null)
                UIDispatcher.BeginExecute(() => PushProgressChanged(this, e));
        }

        /// <summary>
        ///     Raises the <see cref="E:PushCompleted" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncCompletedEventArgs" /> instance containing the event data.</param>
        protected virtual void OnPushCompleted(SyncCompletedEventArgs e)
        {
            if (PushCompleted != null)
                UIDispatcher.BeginExecute(() => PushCompleted(this, e));
        }

        /// <summary>
        ///     Raises the <see cref="E:SyncListBuilt" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncListBuiltEventArgs" /> instance containing the event data.</param>
        protected virtual void OnSyncListBuilt(SyncListBuiltEventArgs e)
        {
            if (SyncListBuilt != null)
                UIDispatcher.BeginExecute(() => SyncListBuilt(this, e));
        }

        /// <summary>
        ///     Raises the <see cref="E:SyncConnection" /> event.
        /// </summary>
        /// <param name="e">The <see cref="SyncConnectionEventArgs" /> instance containing the event data.</param>
        private void OnConnectEvent( SyncConnectionEventArgs e)
        {
            if (SyncConnection != null)
                UIDispatcher.BeginExecute(() => SyncConnection(this, e));
        }

        /// <summary>
        ///     Occurs when the pull has started.
        /// </summary>
        public event EventHandler<SyncStartedEventArgs> PullStarted;

        /// <summary>
        ///     Occurs every time pull has a progress on one file.
        /// </summary>
        public event EventHandler<SyncProgressEventArgs> PullProgressChanged;

        /// <summary>
        ///     Occurs when the pull is completed.
        /// </summary>
        public event EventHandler<SyncCompletedEventArgs> PullCompleted;

        /// <summary>
        ///     Occurs when the push has started.
        /// </summary>
        public event EventHandler<SyncStartedEventArgs> PushStarted;

        /// <summary>
        ///     Occurs every time push has a progress on one file.
        /// </summary>
        public event EventHandler<SyncProgressEventArgs> PushProgressChanged;

        /// <summary>
        ///     Occurs when the push is completed.
        /// </summary>
        public event EventHandler<SyncCompletedEventArgs> PushCompleted;

        /// <summary>
        ///     Occurs when list of files to be synchronized has been built.
        /// </summary>
        public event EventHandler<SyncListBuiltEventArgs> SyncListBuilt;

        /// <summary>
        ///     Occurs if there is a connectivity problem during a push or pull action
        /// </summary>
        public event EventHandler<SyncConnectionEventArgs> SyncConnection;

        #endregion

        internal SyncEnabledStorage(ISyncFileStorage syncFileStorage)
        {
            _syncFileStorage = syncFileStorage;
        }

        internal async Task<ISyncFile> CreateFileAsync(string fileName, ISyncFolder folder, RemoteFolder remoteFolder)
        {
            return await _syncFileStorage.CreateFileAsync(fileName, folder);
        }

        internal async Task<IRandomAccessStream> OpenReadAsync(ISyncFile instance)
        {
            return await _syncFileStorage.OpenReadAsync(instance);
        }

        internal async Task<IRandomAccessStream> OpenWriteAsync(ISyncFile instance)
        {
            return await _syncFileStorage.OpenWriteAsync(instance);
        }

        internal async Task DeleteAsync(ISyncFile instance, RemoteFolder remoteFolder)
        {
            await _syncFileStorage.DeleteAsync(instance);
        }

        internal async Task InsertAndCommitAsync(ISyncFile instance, IRandomAccessStream source,
            RemoteFolder remoteFolder)
        {
            await _syncFileStorage.InsertAndCommitAsync(instance, source);
        }

        internal async Task<IEnumerable<ISyncFile>> ReadAsync(ISyncFolder localFolder, RemoteFolder remoteFolder)
        {
            return await _syncFileStorage.ReadAsync(localFolder);
        }

        internal async Task<ISyncFile> LookupAsync(int id, ISyncFolder syncFolder, RemoteFolder remoteFolder)
        {
            return await _syncFileStorage.LookupAsync(syncFolder, id);
        }

        internal async Task<ISyncFile> LookupAsync(string name, ISyncFolder syncFolder, RemoteFolder remoteFolder)
        {
            return await _syncFileStorage.LookupAsync(syncFolder, name);
        }

        internal async Task UpdateAndCommitAsync(ISyncFile instance, IRandomAccessStream source,
            RemoteFolder remoteFolder)
        {
            await _syncFileStorage.UpdateAndCommitAsync(instance, source);
        }

        internal async Task PushAsync(ISyncFolder syncFolder, RemoteFolder remoteFolder)
        {
            await Task.Run(async () =>
            {
                OnPushStarted(new SyncStartedEventArgs(syncFolder));

                // push those files that are marked as Created, Updated or Deleted
                List<ISyncFile> syncFiles = (await _syncFileStorage.ReadAsync(syncFolder))
                    .Where(p => p.Status == SyncFileStatus.Deleted ||
                                p.Status == SyncFileStatus.Created ||
                                p.Status == SyncFileStatus.Updated).ToList();

                OnSyncListBuilt(new SyncListBuiltEventArgs(syncFolder,
                    syncFiles));

                Parallel.ForEach(syncFiles, syncFile =>
                {
                    RemoteFile remoteFile = null;
                    var result = SyncFileResult.Success;
                    string error = string.Empty;
                    Task<RemoteFile> la = null;
                    try
                    {
                        la = remoteFolder.LookupAsync(syncFile.FileName);
                        la.Wait();
                        remoteFile = la.Result;
                    }
                    catch (SyncConnectionException)
                    {
                        // bad connection
                        // may want to do something here to let the user know the device is not connected
                        // to remote storage, but what happens if the disconnection is only temporary?
                        result = SyncFileResult.Failure;
                        error = "Error Connecting to Remote Storage";
                    }
                    catch (Exception ex)
                    {
                        // there are problems accessing the remote storage for this file
                        // it is assumed that for v1 the necessary retry logic is embedded as part of the remote storage logic
                        // for vNext+1 we may decide that the retry policy is based on a sync policy
                        result = SyncFileResult.Failure;
                        error = ex.Message;
                    }

                    if (remoteFile != null)
                    {
                        // v1 in the absence of a sync policy use the ETag property to determine version status on local and server
                        // vNext+1 sync policies and access conditions will define whether a file gets updated: 
                        //      ETags, MD5 and LastModifiedDate are all properties that can be used to determine whether the operation can be carried out
                        remoteFile.ETag = syncFile.ETag;

                        var syncStatus = SyncFileStatus.AsServer;

                        try
                        {
                            switch (syncFile.Status)
                            {
                                case SyncFileStatus.Deleted:
                                    remoteFolder.DeleteAsync(remoteFile).Wait();
                                    syncStatus = SyncFileStatus.Deleted;
                                    break;
                                case SyncFileStatus.Created:
                                case SyncFileStatus.Updated:
                                    var ora = syncFile.OpenReadAsync();
                                    ora.Wait();
                                    remoteFolder.UploadFromStreamAsync(remoteFile, ora.Result)
                                        .Wait();

                                    // once updated, re-read the remote file so that the local file's properties can be set
                                    remoteFile = la.Result;

                                    syncFile.ETag = remoteFile.ETag;
                                    syncFile.LastModified = remoteFile.LastModified;
                                    syncFile.Uri = remoteFile.Uri;
                                    break;
                            }

                            // only change the status of the local file if the remote file change was successful
                            _syncFileStorage.SyncedAsync(syncFile, syncStatus).Wait();
                        }
                        catch (SyncConnectionException)
                        {
                            // bad connection
                            // may want to do something here to let the user know the device is not connected
                            // to remote storage, but what happens if the disconnection is only temporary?
                            result = SyncFileResult.Failure;
                            error = "Error Connecting to Remote Storage";
                        }
                        catch (Exception ex)
                        {
                            // there are problems accessing the remote storage for this file
                            // it is assumed that for v1 the necessary retry logic is embedded as part of the remote storage logic
                            // for vNext+1 we may decide that the retry policy is based on a sync policy
                            result = SyncFileResult.Failure;
                            error = ex.Message;
                        }
                    }

                    var resultObject = new SyncFileResultObject
                    {
                        SyncFolder = syncFolder,
                        RemoteFile = remoteFile,
                        SyncFile = syncFile,
                        Result = result,
                        Error = error
                    };

                    OnPushProgressChanged(new SyncProgressEventArgs(syncFolder, resultObject.RemoteFile, 
                        resultObject.SyncFile, resultObject.Result, resultObject.Error));
                });

                OnPushCompleted(new SyncCompletedEventArgs(syncFolder));
            });
        }

        internal async Task PullAsync(ISyncFolder syncFolder, RemoteFolder remoteFolder)
        {
            await Task.Run(async () =>
            {
                IEnumerable<RemoteFile> remoteFiles = new List<RemoteFile>();

                try
                {
                    // fetch information about the files from the remote folder
                    remoteFiles = await remoteFolder.ReadAsync();
                }
                catch (SyncConnectionException ex)
                {
                    // bad connection
                    // may want to do something here to let the user know the device is not connected
                    OnConnectEvent(new SyncConnectionEventArgs(syncFolder, ex));
                }
                catch (Exception ex)
                {
                    // there are problems accessing the remote storage for this file
                    // it is assumed that for v1 the necessary retry logic is embedded as part of the remote storage logic
                    // for vNext+1 we may decide that the retry policy is based on a sync policy
                    OnConnectEvent(new SyncConnectionEventArgs(syncFolder, ex));
                }

                OnPullStarted(new SyncStartedEventArgs(syncFolder));

                var filesToDownload = new List<SyncFileResultObject>();

                Parallel.ForEach(remoteFiles, remoteFile =>
                {
                    ISyncFile syncFile = null;
                    var result = SyncFileResult.Success;
                    string error = string.Empty;

                    try
                    {
                        var la = _syncFileStorage.LookupAsync(syncFolder, remoteFile.FileName);
                        la.Wait();
                        syncFile = la.Result;

                        if (syncFile == null)
                        {
                            try
                            {
                                var ca = _syncFileStorage.CreateFileAsync(remoteFile.FileName, syncFolder);
                                ca.Wait();
                                syncFile = ca.Result;
                            }
                            catch (Exception)
                            {
                                // the file exists in the sync folder but there is no record of the file, i.e. its state was never created
                            }
                        }
                        else
                        {
                            // is the sync file AsServer
                            if (syncFile.Status == SyncFileStatus.AsServer)
                            {
                                // compare the ETag
                                if (!syncFile.ETag.Equals(remoteFile.ETag))
                                {
                                    // the file on the server is different so download
                                    syncFile = la.Result;
                                }
                                else
                                {
                                    // the file is the same as on the server so no need to download
                                    result = SyncFileResult.Failure;
                                    error = "Local and Server file already match";
                                }
                            }
                        }
                    }
                    catch (SyncConnectionException)
                    {
                        // bad connection
                        // may want to do something here to let the user know the device is not connected
                        // to remote storage, but what happens if the disconnection is only temporary?
                        result = SyncFileResult.Failure;
                        error = "Error Connecting to Remote Storage";
                    }
                    catch (Exception ex)
                    {
                        // there are problems accessing the remote storage for this file
                        // it is assumed that for v1 the necessary retry logic is embedded as part of the remote storage logic
                        // for vNext+1 we may decide that the retry policy is based on a sync policy
                        result = SyncFileResult.Failure;
                        error = ex.Message;
                    }

                    filesToDownload.Add(new SyncFileResultObject
                    {
                        SyncFolder = syncFolder,
                        RemoteFile = remoteFile,
                        SyncFile = syncFile,
                        Result = result,
                        Error = error
                    });
                });

                OnSyncListBuilt(
                    new SyncListBuiltEventArgs(syncFolder, filesToDownload.Select(p => p.SyncFile)));

                Parallel.ForEach(filesToDownload, file =>
                {
                    if (file.Result == SyncFileResult.Success)
                    {
                        var owa = file.SyncFile.OpenWriteAsync();
                        owa.Wait();
                        using (IRandomAccessStream stream = owa.Result)
                        {
                            try
                            {
                                file.RemoteFile.DownloadToStreamAsync(stream).Wait();
                            }
                            catch (SyncConnectionException)
                            {
                                // bad connection
                                // may want to do something here to let the user know the device is not connected
                                // to remote storage, but what happens if the disconnection is only temporary?
                                file.Result = SyncFileResult.Failure;
                                file.Error = "Error Connecting to Remote Storage";
                            }
                            catch (Exception ex)
                            {
                                // there are problems accessing the remote storage for this file
                                // it is assumed that for v1 the necessary retry logic is embedded as part of the remote storage logic
                                // for vNext+1 we may decide that the retry policy is based on a sync policy
                                file.Result = SyncFileResult.Failure;
                                file.Error = ex.Message;
                            }
                        }

                        file.SyncFile.ETag = file.RemoteFile.ETag;
                        file.SyncFile.LastModified = file.RemoteFile.LastModified;
                        file.SyncFile.Uri = file.RemoteFile.Uri;

                        // once the download is complete mark the file as being the same as the server
                        _syncFileStorage.SyncedAsync(file.SyncFile, SyncFileStatus.AsServer).Wait();

                        OnPullProgressChanged(new SyncProgressEventArgs(syncFolder, file.RemoteFile,
                            file.SyncFile, file.Result,
                            file.Error));
                    }
                });

                OnPullCompleted(new SyncCompletedEventArgs(syncFolder));
            });
        }

        // not updates to remote folder - local only
        internal async Task TruncateAsync(ISyncFolder localFolder)
        {
            await _syncFileStorage.TruncateAsync(localFolder);
        }

        // not updates to remote folder - local only
        internal async Task<IStorageFile> GetInternalStorageFile(ISyncFile instance)
        {
            return await _syncFileStorage.GetInternalStorageFile(instance);
        }

        // not updates to remote folder - local only
        internal async Task FlushAsync(SyncFolder syncFolder)
        {
            await _syncFileStorage.FlushAsync(syncFolder);
        }

        // not updates to remote folder - local only
        internal async Task CommitAsync(ISyncFile instance)
        {
            await _syncFileStorage.CommitAsync(instance);
        }

        // not updates to remote folder - local only
        internal async Task CancelAsync(SyncFolder syncFolder)
        {
            await _syncFileStorage.CancelAsync(syncFolder);
        }

        // not updates to remote folder - local only
        internal async Task UndoAsync(ISyncFile instance)
        {
            await _syncFileStorage.UndoAsync(instance);
        }
    }

    internal class SyncFileResultObject
    {
        internal RemoteFile RemoteFile { get; set; }

        internal ISyncFile SyncFile { get; set; }

        internal ISyncFolder SyncFolder { get; set; }

        internal SyncFileResult Result { get; set; }

        internal string Error { get; set; }
    }
}